{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "# 1. Decorators 101: Core Concepts\n",
        "\n",
        "A **decorator** in Python is a **higher-order function** that takes another function (or class) as an argument, wraps it with additional behavior, and returns a new function (or class).\n",
        "\n",
        "## 1.1 Anatomy of a Basic Function Decorator"
      ],
      "metadata": {
        "id": "lmQJpXpfX_d0"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def my_decorator(func):\n",
        "    def wrapper(*args, **kwargs):\n",
        "        print(\"Do something before\")\n",
        "        result = func(*args, **kwargs)\n",
        "        print(\"Do something after calling function\")\n",
        "        return result\n",
        "    return wrapper\n",
        "\n",
        "@my_decorator\n",
        "def greet_me(name: str) -> str:\n",
        "    return f\"Hello, {name}!\"\n",
        "\n",
        "greet_me(\"Junaid\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 71
        },
        "id": "5Q7STz56YBbJ",
        "outputId": "7bd15a19-407a-4734-c8e3-83ee309e1f84"
      },
      "execution_count": 14,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Do something before\n",
            "Do something after calling function\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "'Hello, Junaid!'"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            }
          },
          "metadata": {},
          "execution_count": 14
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "When you use the **`@my_decorator`** syntax:\n",
        "\n",
        "- Python passes the function `greet` to `my_decorator`, which returns `wrapper`.\n",
        "- The name `greet` in your code then references `wrapper`.\n",
        "- Calling `greet(\"Alice\")` actually calls `wrapper(\"Alice\")`.\n",
        "\n",
        "### Example 1: Simple Logging Decorator"
      ],
      "metadata": {
        "id": "K7J8tX6KYUIU"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def log_calls(func):\n",
        "    \"\"\"Logger Decorator\"\"\"\n",
        "    def wrapper(*args, **kwargs):\n",
        "        print(f\"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}\")\n",
        "        result = func(*args, **kwargs)\n",
        "        print(f\"[LOG] {func.__name__} returned {result}\")\n",
        "        return result\n",
        "    return wrapper\n",
        "\n",
        "@log_calls\n",
        "def add(a: int, b: int) -> int:\n",
        "  \"\"\"Add \"\"\"\n",
        "  return a + b\n",
        "\n",
        "add(3, 5)\n",
        "print(f\"What about Metadata {add.__doc__}\")\n",
        "# [LOG] Calling add with args=(3, 5), kwargs={}\n",
        "# [LOG] add returned 8"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5FSMLclfYXPO",
        "outputId": "27fecfd7-a3c0-4f0b-bd3f-56f4a82145df"
      },
      "execution_count": 19,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "[LOG] Calling add with args=(3, 5), kwargs={}\n",
            "[LOG] add returned 8\n",
            "What about Metadata None\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "**Key takeaway**: Decorators let you **interject** extra logic (e.g., logging, timing, caching) around function calls.\n",
        "\n",
        "---\n",
        "\n",
        "## 1.2 Decorators with Arguments\n",
        "\n",
        "Sometimes you want a **parametrized decorator**. This requires an extra “factory” function that takes your decorator arguments and returns the real decorator.\n",
        "\n",
        "### Example 2: Repetition Decorator"
      ],
      "metadata": {
        "id": "2yo7TmuAYasX"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def repeat(times: int):\n",
        "    \"\"\"A decorator factory that repeats a function call 'times' times.\"\"\"\n",
        "    def decorator(func):\n",
        "        def wrapper(*args, **kwargs):\n",
        "            result = None\n",
        "            for _ in range(times):\n",
        "                result = func(*args, **kwargs)\n",
        "            return result\n",
        "        return wrapper\n",
        "    return decorator\n",
        "\n",
        "@repeat(times=3)\n",
        "def greet(name: str):\n",
        "    print(f\"Hello, {name}!\")\n",
        "\n",
        "greet(\"Alice\")\n",
        "# Hello, Alice!\n",
        "# Hello, Alice!\n",
        "# Hello, Alice!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "UN3HDbHdYdEr",
        "outputId": "5edae8f0-d240-458e-be35-b64c71c1de23"
      },
      "execution_count": 16,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Hello, Alice!\n",
            "Hello, Alice!\n",
            "Hello, Alice!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "- **`repeat(times=3)`** returns the actual decorator.\n",
        "- That decorator wraps `greet`, repeating the call 3 times.\n",
        "\n",
        "---\n",
        "\n",
        "## 1.3 Preserving Metadata with `functools.wraps`\n",
        "\n",
        "A subtlety with decorators is that they can overwrite the original function’s metadata (`__name__`, `__doc__`, etc.). Use `@functools.wraps(original_func)` in the wrapper to preserve these.\n",
        "\n"
      ],
      "metadata": {
        "id": "jCVk3-07YhIu"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import functools\n",
        "\n",
        "def logger(func):\n",
        "    @functools.wraps(func)\n",
        "    def wrapper(*args, **kwargs):\n",
        "        \"\"\"Wrapper docstring\"\"\"\n",
        "        print(f\"Calling {func.__name__}...\")\n",
        "        return func(*args, **kwargs)\n",
        "    return wrapper\n",
        "\n",
        "@logger\n",
        "def example_function(x: int) -> int:\n",
        "    \"\"\"Original docstring of example_function.\"\"\"\n",
        "    return x * 2\n",
        "\n",
        "print(example_function.__name__)  # \"example_function\"\n",
        "print(example_function.__doc__)   # \"Original docstring of example_function.\""
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ppQXz-oXYj69",
        "outputId": "0985ab32-4e61-4855-9caa-1718e138915d"
      },
      "execution_count": 20,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "example_function\n",
            "Original docstring of example_function.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "---\n",
        "\n",
        "# 2. Decorators with DataClasses\n",
        "\n",
        "Data classes (`@dataclass`) automatically generate special methods like `__init__`, `__repr__`, and `__eq__` based on class attributes. We can use **decorators** to:\n",
        "\n",
        "1. **Track** or **modify** a data class’s behavior (class decorators),\n",
        "2. **Add** logic to **methods** within a data class (method decorators).\n"
      ],
      "metadata": {
        "id": "qlV6RLKZYmCY"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Add logic to methods within a data class (method decorators).\n",
        "\n",
        "import functools\n",
        "from dataclasses import dataclass\n",
        "\n",
        "def log_method_call(func):\n",
        "    @functools.wraps(func)\n",
        "    def wrapper(self, *args, **kwargs):\n",
        "        print(f\"[DEBUG] Method {func.__name__} called on {type(self).__name__}\")\n",
        "        return func(self, *args, **kwargs)\n",
        "    return wrapper\n",
        "\n",
        "@dataclass\n",
        "class BankAccount:\n",
        "    owner: str\n",
        "    balance: float = 0.0\n",
        "\n",
        "    @log_method_call\n",
        "    def deposit(self, amount: float) -> None:\n",
        "        self.balance += amount\n",
        "\n",
        "    @log_method_call\n",
        "    def withdraw(self, amount: float) -> None:\n",
        "        if amount > self.balance:\n",
        "            raise ValueError(\"Insufficient funds.\")\n",
        "        self.balance -= amount\n",
        "\n",
        "\n",
        "acct = BankAccount(\"Alice\", 100)\n",
        "acct.deposit(50)\n",
        "# [DEBUG] Method deposit called on BankAccount\n",
        "acct.withdraw(20)\n",
        "# [DEBUG] Method withdraw called on BankAccount\n",
        "\n",
        "print(acct)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ZYZRr8OzYrOJ",
        "outputId": "f10778d6-fea8-4461-aefa-2129f84577b8"
      },
      "execution_count": 29,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "[DEBUG] Method deposit called on BankAccount\n",
            "[DEBUG] Method withdraw called on BankAccount\n",
            "BankAccount(owner='Alice', balance=130)\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Here:\n",
        "1. `@log_method_call` logs each method call before running the actual logic.\n",
        "2. We target **only** specific methods within the data class.\n",
        "\n",
        "---\n",
        "\n",
        "# 3. Decorators + Generics\n",
        "\n",
        "## 3.1 Decorators for Generic Functions\n",
        "\n",
        "When we say “generics,” we typically reference typed containers or functions that rely on `TypeVar`. A decorator can handle a generic function, but note that many Python type checkers might require you to carefully handle the function signature.\n",
        "\n",
        "### Example 5: Generic Decorator for Re-Usable Logic\n"
      ],
      "metadata": {
        "id": "u7EFD4dDY3o2"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar, Generic, Callable\n",
        "import functools\n",
        "\n",
        "T = TypeVar(\"T\")\n",
        "R = TypeVar(\"R\")\n",
        "\n",
        "def ensure_positive_result(func: Callable[[T], R]) -> Callable[[T], R]:\n",
        "    \"\"\"\n",
        "    A generic decorator that ensures the result of the wrapped function is positive.\n",
        "    Raises ValueError if not.\n",
        "    \"\"\"\n",
        "    @functools.wraps(func)\n",
        "    def wrapper(param: T) -> R:\n",
        "        result = func(param)\n",
        "        if isinstance(result, (int, float)) and result < 0:\n",
        "            raise ValueError(f\"Result must be positive, got {result}\")\n",
        "        return result\n",
        "    return wrapper\n",
        "\n",
        "@ensure_positive_result\n",
        "def half(x: float) -> float:\n",
        "    return x / 2.0\n",
        "\n",
        "print(half(10.0))  # 5.0"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "wBF8jASeY5zX",
        "outputId": "edb1f654-1c47-4d98-dbe6-147f02759d64"
      },
      "execution_count": 30,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "5.0\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# print(half(-1.0)) # Raises ValueError"
      ],
      "metadata": {
        "id": "0_K5gB8wagWm"
      },
      "execution_count": 32,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "**Explanation**:\n",
        "1. We define `ensure_positive_result(func)` as a function that returns a `Callable[[T], R]`, using generics for the input/return types.  \n",
        "2. The decorator checks if the returned value is numeric and negative; if so, it raises an error.  \n",
        "3. This can work for any function that returns a numeric result—**type checkers** will enforce that the function’s signature matches `(T) -> R`."
      ],
      "metadata": {
        "id": "ojuMprktY9f2"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 4. Putting It All Together\n",
        "\n",
        "Decorators can:\n",
        "1. **Wrap functions** to add logging, caching, validation, or other cross-cutting concerns.\n",
        "2. **Wrap classes** (including **data classes**) to alter how they’re initialized, track instances, or impose constraints.\n",
        "3. **Use generics** so they stay flexible for different function signatures or data class type parameters.\n",
        "\n",
        "**Key Best Practices**:\n",
        "- **Use `functools.wraps`** when decorating functions. This preserves important metadata.  \n",
        "- **Document your decorators** carefully, especially if they’re generic or if they modify class behavior in unexpected ways.  \n",
        "- **Mind the order** of multiple decorators. Python applies them **top-down**. For instance,  \n",
        "  ```python\n",
        "  @decoratorA\n",
        "  @decoratorB\n",
        "  def my_func(): ...\n",
        "  ```  \n",
        "  is equivalent to  \n",
        "  ```python\n",
        "  my_func = decoratorA(decoratorB(my_func))\n",
        "  ```\n"
      ],
      "metadata": {
        "id": "aEehryucX8A6"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "- **Test thoroughly** if your decorator modifies function or class behavior that can affect state or type expectations.\n",
        "\n",
        "---\n",
        "\n",
        "# Final Summary\n",
        "\n",
        "1. **Function Decorators**: Great for adding common logic (logging, validation, caching) around any callable.  \n",
        "2. **Class Decorators**: Especially useful for data classes, letting you inject or modify their behavior upon instantiation.  \n",
        "3. **Generics**: Allow decorators to remain type-safe across a variety of parameter and return types, critical in large, type-hinted Python codebases.  \n",
        "4. **DataClasses + Decorators**: You can decorate individual methods inside a data class or decorate the class as a whole for advanced initialization/validation.  \n",
        "\n",
        "This **comprehensive** approach—blending **decorators**, **data classes**, and **generics**—helps you write more **modular**, **scalable**, and **maintainable** Python applications, including advanced use cases in AI, web backends, or any data-driven system. Experiment, refine, and enjoy the powerful metaprogramming capabilities Python offers!"
      ],
      "metadata": {
        "id": "tfd_PNszX13S"
      }
    }
  ]
}